use chrono::NaiveDateTime;
use uuid::Uuid;

use super::{errors::JobError, id::JobId};

/// Unchanging job metadata for identifuing it.
#[derive(Clone, Debug)]
pub struct JobMeta {
    /// Job ID uniquely identifying it across all jobs.
    id: JobId,

    /// Time the job was created.
    created_at: NaiveDateTime,

    /// Job type, corresponding to queue name.
    job_type: String,
}

impl JobMeta {
    pub fn new(id: JobId, created_at: NaiveDateTime, job_type: String) -> Self {
        Self {
            id,
            created_at,
            job_type,
        }
    }

    pub fn id(&self) -> JobId {
        self.id
    }

    pub fn created_at(&self) -> &NaiveDateTime {
        &self.created_at
    }

    pub fn job_type(&self) -> &str {
        &self.job_type
    }
}

/// Job metadata plus the payload.
#[derive(Clone, Debug)]
pub struct JobData {
    /// Metadata about the job.
    meta: JobMeta,

    /// Specific job payload.
    payload: Vec<u8>,
}

impl JobData {
    pub fn new(meta: JobMeta, payload: Vec<u8>) -> Self {
        Self { meta, payload }
    }

    pub fn meta(&self) -> &JobMeta {
        &self.meta
    }

    pub fn payload(&self) -> &[u8] {
        &self.payload
    }

    pub fn id(&self) -> JobId {
        self.meta().id
    }

    pub fn created_at(&self) -> &NaiveDateTime {
        &self.meta().created_at
    }

    pub fn job_type(&self) -> &str {
        &self.meta().job_type
    }
}

pub struct PendingJobEntry {
    /// Biographical job data.
    data: JobData,

    /// Number of tries attempted as of the time we queried it.  This is an i16
    /// because it's trying to match the representation in the database.
    num_tries: i16,

    /// Time of our last try, if there is one.
    last_try: Option<NaiveDateTime>,
}

impl PendingJobEntry {
    pub fn new(data: JobData, num_tries: i16, last_try: Option<NaiveDateTime>) -> Self {
        Self {
            data,
            num_tries,
            last_try,
        }
    }

    pub fn data(&self) -> &JobData {
        &self.data
    }

    pub fn id(&self) -> Uuid {
        self.data().id()
    }

    pub fn num_tries(&self) -> i16 {
        self.num_tries
    }

    pub fn last_try(&self) -> Option<&NaiveDateTime> {
        self.last_try.as_ref()
    }

    pub fn get_run_context(&self) -> RunContext {
        RunContext {
            num_prev_tries: self.num_tries as u16,
            last_try: self.last_try,
        }
    }
}

#[derive(Clone, Debug)]
pub struct JobCompletionData {
    /// When we decided that the job was completed, in UTC.
    completed_at: NaiveDateTime,

    /// Job completion status.
    status: i16,

    /// Optional message, usually describing an error condition.
    msg: Option<String>,
}

impl JobCompletionData {
    pub fn new(completed_at: NaiveDateTime, status: i16, msg: Option<String>) -> Self {
        Self {
            completed_at,
            status,
            msg,
        }
    }

    /// Creates a new instance for a successful job, using the current time.
    pub fn new_success_now() -> Self {
        Self {
            completed_at: chrono::Utc::now().naive_utc(),
            status: 0,
            msg: None,
        }
    }

    /// Creates a new instance for a failed job, using the current time and
    /// formatting the error as a message.
    pub fn new_from_err<E: ToString>(status: i16, e: &E) -> Self {
        Self {
            completed_at: chrono::Utc::now().naive_utc(),
            status,
            msg: Some(e.to_string()),
        }
    }

    /// Creates a new instance from a job error, using the status code and
    /// display message from it.
    pub fn from_job_err(e: &JobError) -> Self {
        Self::new_from_err(e.status_code(), e)
    }

    pub fn completed_at(&self) -> &NaiveDateTime {
        &self.completed_at
    }

    pub fn status(&self) -> i16 {
        self.status
    }

    pub fn msg(&self) -> Option<&str> {
        self.msg.as_ref().map(|s| s.as_ref())
    }

    pub fn into_msg(self) -> Option<String> {
        self.msg
    }

    /// Returns if the job was successful.  Ie. if the status is 0.
    pub fn is_successful(&self) -> bool {
        self.status == 0
    }
}

/// Tracks history of recent attempts at running the job.
#[derive(Clone, Debug)]
pub struct RunContext {
    num_prev_tries: u16,
    last_try: Option<NaiveDateTime>,
}

impl RunContext {
    pub fn new_fresh() -> Self {
        Self {
            num_prev_tries: 0,
            last_try: None,
        }
    }

    pub fn num_prev_tries(&self) -> u16 {
        self.num_prev_tries
    }

    pub fn last_try(&self) -> Option<&NaiveDateTime> {
        self.last_try.as_ref()
    }
}

/// Configuration relating to how we handle retrying and completion.
#[derive(Clone, Debug)]
pub struct JobConfig {
    /// Base amount of time to wait before retrying for the first time.
    pub base_backoff_time_ms: u32,

    /// Factor we multiply the backoff time by and then div by 100 to compute next backoff duration.
    pub backoff_factor: u16,

    /// Maximum number of retries before aborting.
    pub retry_limit: u16,

    /// Number of jobs that can be executing simultaneously.
    pub parallel_jobs: usize,

    /// If we should keep the payloads of jobs that were successful.
    pub keep_ok_payloads: bool,

    /// If we should keep the payloads of jobs that failed.
    pub keep_err_payloads: bool,
}

#[derive(Clone, Debug)]
pub struct FailEntry {
    /// Autobiographical job data.
    meta: JobMeta,

    /// Payload, which might have been pruned.
    payload: Option<Vec<u8>>,

    /// Completion status data.
    completion: JobCompletionData,
}

impl FailEntry {
    pub fn new(meta: JobMeta, payload: Option<Vec<u8>>, completion: JobCompletionData) -> Self {
        Self {
            meta,
            payload,
            completion,
        }
    }
}
